#ifndef EDITOR_H
#define EDITOR_H
#include "file.hpp"
#include <stack>
#include <string>
#include <ncurses.h>
using namespace std;
struct EditorState
{
    File f;
    int x, y, rowoffset;
    EditorState() {}
    EditorState(const string &filename)
    {
        x = 0;
        y = 0;
        rowoffset = 0;
        f = File(filename);
        f.open();
    }
    EditorState(const EditorState &other)
    {
        f = File(other.f);
        x = other.x;
        y = other.y;
        rowoffset = other.rowoffset;
    }
};
enum Mode
{
    NORMAL,
    INSERT,
    SEARCH,
    REPLACE
};
enum Color
{
    WHITE = 0,
    BULUE = 1,
    YELLOW = 2,
    RED = 3
};
class Editor
{
public:
    Editor(const string &name)
    {
        init(name);
    }
    ~Editor()
    {
        endwin();
    }
    void run()
    {
        int ch;
        while (!_exit)
        {
            getWindowSize(width, height);
            erase();
            updateUI();
            move(e.y - e.rowoffset, e.x);
            refresh();
            ch = getch();
            switch (mode)
            {
            case NORMAL:
                handleNormalInput(ch);
                break;
            case INSERT:
                handleInsertInput(ch);
                break;
            case SEARCH:
                handleSearchInput(ch);
                break;
            case REPLACE:
                handleReplaceInput(ch);
                break;
            default:
                break;
            }
        }
    }

private:
    void init(const string &name)
    {
        initscr();
        cbreak();
        noecho();
        keypad(stdscr, TRUE);
        start_color();
        getWindowSize(width, height);
        int background = getbkgd(stdscr);
        short color_pair = background & A_COLOR;
        short color = color_pair & 0xFF;
        init_pair(WHITE, COLOR_WHITE, color);
        init_pair(YELLOW, COLOR_YELLOW, color);
        init_pair(BULUE, COLOR_BLUE, color);
        init_pair(RED, COLOR_RED, color);
        mode = NORMAL;
        _exit = false;
        e = EditorState(name);
        updateUndoAndRedoStack();
    }
    void getWindowSize(int &width, int &height)
    {
        getmaxyx(stdscr, height, width);
        height -= 2;
        width *= 0.75;
    }
    void handleNormalInput(int input)
    {
        switch (input)
        {
        case 'h':
        case KEY_LEFT:
            left();
            break;
        case 'j':
        case KEY_DOWN:
            down();
            break;
        case 'k':
        case KEY_UP:
            up();
            break;
        case 'l':
        case KEY_RIGHT:
            right();
            break;
        case 'i':
            switchMode(INSERT);
            break;
        case 'u':
            undo();
            saved = false;
            break;
        case 'r':
            redo();
            saved = false;
            break;
        case 'y':
            coppyRow();
            break;
        case 'p':
            pasteRow();
            updateUndoAndRedoStack();
            saved = false;
            break;
        case 'd':
            eraseRow();
            updateUndoAndRedoStack();
            saved = false;
            break;
        case 'w':
            save();
            break;
        case 'q':
            exit();
            break;
        case 'x':
            eraseChar();
            updateUndoAndRedoStack();
            saved = false;
            break;
        case '/':
            switchMode(SEARCH);
            break;
        case 't':
            switchMode(REPLACE);
            break;
        }
    }
    void handleInsertInput(int input)
    {
        switch (input)
        {
        case 27: // Esc
            switchMode(NORMAL);
            break;
        case KEY_UP:
            up();
            break;
        case KEY_DOWN:
            down();
            break;
        case KEY_LEFT:
            left();
            break;
        case KEY_RIGHT:
            right();
            break;
        case KEY_BACKSPACE:
            eraseChar();
            updateUndoAndRedoStack();
            saved = false;
            break;
        case 10: // enter
            enter();
            updateUndoAndRedoStack();
            saved = false;
            break;
        default:
            insertChar((char)input);
            updateUndoAndRedoStack();
            saved = false;
            break;
        }
    }
    void handleSearchInput(int input)
    {
        switch (input)
        {
        case KEY_BACKSPACE:
            if (target.size() > 0)
                target.pop_back();
            break;
        case 27:
        case 10:
            switchMode(NORMAL);
            break;
        default:
            target += (char)(input);
            break;
        }
    }
    void handleReplaceInput(int input)
    {
        switch (input)
        {
        case 27: // Esc
            cmdBuffer.clear();
            target.clear();
            switchMode(NORMAL);
            break;
        case 10: // Enter
            replace();
            cmdBuffer.clear();
            target.clear();
            switchMode(NORMAL);
            break;
        case KEY_BACKSPACE:
            if (cmdBuffer.size() > 0)
                cmdBuffer.pop_back();
            break;
        default:
            cmdBuffer += (char)input;
            break;
        }
        int idx = cmdBuffer.size() - 1;
        while (idx >= 0 && cmdBuffer[idx] != '/')
            idx--;
        if (idx < 0)
            target = cmdBuffer;
        else
            target = cmdBuffer.substr(0, idx);
    }
    void switchMode(Mode newMode)
    {
        mode = newMode;
    }
    void updateUI()
    {
        curs_set(0);
        int row = 0;
        for (row = 0; row < height && row + e.rowoffset < e.f.getRowCount(); row++)
        {
            string s = e.f.get(row + e.rowoffset);
            mvprintw(row, 0, "%s", s.c_str());
        }
        if (!target.empty())
        {
            int m = target.size();
            int *next = getNext();
            for (int y = 0; y < height && y + e.rowoffset < e.f.getRowCount(); y++)
            {
                string s = e.f.get(y + e.rowoffset);
                int n = s.size();
                for (int x = 0, j = 0; x < n; x++)
                {
                    while (s[x] != target[j] && j > 0)
                        j = next[j - 1];
                    if (s[x] == target[j])
                        j++;
                    if (j == m)
                    {
                        attron(COLOR_PAIR(YELLOW));
                        mvprintw(y, x - m + 1, "%s", target.c_str());
                        attroff(COLOR_PAIR(YELLOW));
                        j = 0;
                    }
                }
            }
            delete[] next;
        }
        while (row < height)
        {
            attron(COLOR_PAIR(BULUE));
            mvprintw(row++, 0, "~");
            attroff(COLOR_PAIR(BULUE));
        }
        if (mode == NORMAL)
            mvprintw(height + 1, 0, "--NORMAL--");
        else if (mode == INSERT)
            mvprintw(height + 1, 0, "--INSERT--");
        else if (mode == SEARCH)
            mvprintw(height + 1, 0, "--SEARCH--");
        else if (mode == REPLACE)
            mvprintw(height + 1, 0, "--REPLACE--");
        if (e.f.getRowCount() > 99999)
            mvprintw(height + 1, width - 6, "-----");
        else
            mvprintw(height + 1, width - 6, "%d", e.f.getRowCount());
        mvprintw(height + 1, width - 7, ",");
        if (e.f.getCharCount() > 99999)
            mvprintw(height + 1, width - 12, "-----");
        else
            mvprintw(height + 1, width - 12, "%ld", e.f.getCharCount());
        mvprintw(height + 1, width - 13 - e.f.getName().size(), "%s", e.f.getName().c_str());
        if (!saved)
            mvprintw(height + 1, width - 14 - e.f.getName().size(), "*");
        if (mode == SEARCH)
            mvprintw(height, 0, "%s", target.c_str());
        else if (mode == REPLACE)
            mvprintw(height, 0, "%s", cmdBuffer.c_str());
        curs_set(1);
    }
    int *getNext()
    {
        int m = target.size();
        int *next = new int[m];
        next[0] = 0;
        for (int j = 0, i = 1; i < m; i++)
        {
            while (target[i] != target[j] && j > 0)
                j = next[j - 1];
            if (target[i] == target[j])
                j++;
            next[i] = j;
        }
        return next;
    }
    void left()
    {
        if (e.x == 0 && e.y > 0)
        {
            e.x = e.f.get(e.y - 1).size();
            up();
        }
        else if (e.x > 0)
            e.x--;
    }
    void up()
    {
        if (e.y == e.rowoffset)
            e.rowoffset = max(e.rowoffset - 1, 0);
        e.y = max(e.y - 1, 0);
        e.x = min(e.x, (int)e.f.get(e.y).size());
    }
    void down()
    {
        if (e.y == e.rowoffset + height - 1)
            e.rowoffset = min(e.rowoffset + 1, e.f.getRowCount() - height);
        e.y = min(e.y + 1, e.f.getRowCount() - 1);
        e.x = min(e.x, (int)e.f.get(e.y).size());
    }
    void right()
    {
        if (e.x == (int)e.f.get(e.y).size() && e.y < e.f.getRowCount() - 1)
        {
            e.x = 0;
            down();
        }
        else if (e.x < (int)e.f.get(e.y).size())
            e.x++;
    }
    void undo()
    {
        if (undoStack.size() > 1)
        {
            redoStack.push(EditorState(e));
            undoStack.pop();
            e = EditorState(undoStack.top());
        }
    }
    void redo()
    {
        if (!redoStack.empty())
        {
            e = EditorState(redoStack.top());
            redoStack.pop();
            undoStack.push(EditorState(e));
        }
    }
    void coppyRow()
    {
        coppy = e.f.get(e.y);
    }
    void insertRow(const string &s)
    {
        e.f.insertRow(s, e.y);
    }
    void insertChar(char ch)
    {
        if ((int)e.f.get(e.y).size() >= width)
        {
            e.f.insertChar(ch, e.y, e.x);
            e.f.updateOverLengthRow(e.y, width);
            right();
            e.x = max(e.x, 1);
        }
        else
        {
            e.f.insertChar(ch, e.y, e.x);
            e.x++;
        }
    }
    void eraseRow()
    {
        e.f.eraseRow(e.y);
        if (e.f.empty())
            e.f.insertRow("", 0);
        up();
    }
    void eraseChar()
    {
        if (e.x == 0 && e.y > 0)
        {
            e.x = e.f.get(e.y - 1).size();
            e.f.backUp(e.y);
            up();
            e.f.updateOverLengthRow(e.y, width);
        }
        else if (e.x > 0)
        {
            e.f.eraseChar(e.y, e.x - 1);
            e.x--;
        }
    }
    void enter()
    {
        e.f.enter(e.y, e.x);
        down();
        e.x = 0;
    }
    void pasteRow()
    {
        if (coppy.empty())
            return;
        insertRow(coppy);
    }
    void save()
    {
        ofstream outFile(e.f.getName());
        if (outFile.is_open())
        {
            for (int i = 0; i < e.f.getRowCount(); i++)
                outFile << e.f.get(i) << endl;
            outFile.close();
            saved = true;
        }
    }
    void exit()
    {
        if (!saved)
        {
            mvprintw(height, 0, "unsaved,press 'q' to quit");
            move(e.y - e.rowoffset, e.x);
            refresh();
            int ch = getch();
            if (ch == 'q')
                _exit = true;
        }
        else
        {
            _exit = true;
        }
    }
    void replace()
    {
        int idx = cmdBuffer.size() - 1;
        while (idx >= 0 && cmdBuffer[idx] != '/')
            idx--;
        if (idx < 0)
            return;
        e.f.replace(target, cmdBuffer.substr(idx + 1));
        updateUndoAndRedoStack();
        saved = false;
        e.x = min(e.x, (int)e.f.get(e.y).size());
        for (int i = 0; i < e.f.getRowCount(); i++)
            e.f.updateOverLengthRow(i, width);
    }
    void updateUndoAndRedoStack()
    {
        undoStack.push(EditorState(e));
        while (!redoStack.empty())
            redoStack.pop();
    }
    EditorState e;
    int width, height;
    stack<EditorState> undoStack, redoStack;
    string target, coppy, cmdBuffer;
    bool _exit, saved;
    Mode mode;
};
#endif
